#!/usr/bin/env bash

shopt -s extglob

# Upstream wrapper requires UUID to be used for configuration.

# However, when declaratively describing a host, we may not know its UUID, and
# shouldn't need to persist something that will differ between hosts built from
# the same configuration template.

# Thus, for using bees from NixOS, we have our own wrapper, which supports not
# just UUID but any specification permitted by findmnt

[[ $bees_debug ]] && { PS4=':${BASH_SOURCE##*/}:$LINENO+'; set -x; }

usage() {
  cat >&2 <<EOF
Usage: ${BASH_SOURCE##*/} run|cleanup config-name|fsSpec [idxSizeMB=...] [verbosity=...] [workDir=...] [-- daemon-options...]

  fsSpec should be in a format recognized by findmnt. Alternately,
  "config-name" may refer to a file that exists in ${bees_config_dir:-/etc/bees}
  with a .conf extension; if that file does not specify UUID, findmnt will be
  used in addition.

  Note that while config files may presently use shell arithmetic, use of this
  functionality is not encouraged going forward: Setting ''idxSizeMB=4096'' is
  preferred over ''DB_SIZE=$((1024*1024*1024*4))'' or ''DB_SIZE=$(( AL16M * 256 ))'',
  although both of these are presently supported.

  If fsSpec contains a /, it assumed to be a mount point to be looked up by
  findmnt, not a config file name.

  daemon-options are passed directly through to the daemon on startup, as
  documented at https://github.com/Zygo/bees/blob/master/docs/options.md.
EOF
  exit 1
}

die() { echo "$*" >&2; exit 1; }

allConfigNames=( blockdev fsSpec home idxSize idxSizeMB mntDir runDir status verbosity workDir )

# Alternate names for configuration values; "bees_" will always be prepended
declare -A altConfigNames=(
  # from original bees wrapper
  [BEESHOME]=home
  [BEESSTATUS]=status
  [MNT_DIR]=mntDir
  [UUID]=uuid
  [WORK_DIR]=runDir
  [DB_SIZE]=idxSize
)

# legacy bees config files can be arbitrary shell scripts, so we need to actually evaluate them
sandboxedConfigFileEval() {
  bash_exe=$(type -P bash) || exit
  PATH=/var/empty ENV='' BASH_ENV='' AL128K="$((128*1024))" AL16M="$((16*1024*1024))" "$bash_exe" -r ${bees_debug+-x} \
    -c 'eval "$(</dev/stdin)" >&2; for var; do [[ ${!var} ]] && printf "%q=%s\\0" "$var" "${!var}"; done' \
    "${!altConfigNames[@]}" "${allConfigNames[@]}" \
    <"$1"
}

readConfigFileIfExists() {
  local line
  [[ -s $1 ]] || return 1
  while IFS= read -r -d '' line; do
    line=${line%%+([[:space:]])"#"*}
    [[ $line ]] || continue
    [[ $line = *=* ]] || {
      printf 'WARNING: Config file line not recognized: %q\n' "$line" >&2
      continue
    }
    set_option "$line"
  done < <(sandboxedConfigFileEval "$1")
}

set_option() {
  local k v
  k="${1%%=*}" v="${1#*=}"
  [[ ${altConfigNames[$k]} ]] && k=${altConfigNames[$k]}
  printf -v "bees_$k" %s "$v"
}

uuid_re='^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$'

# Shared code for setting configuration used by other operations.
#
# Reads from global associative array "opts" containing options passed in as
# key=value pairs on the command line, looks for config-file overrides, and
# sets individual global variables.
_setup() {
  declare fstype
  bees_fsSpec=$1; shift

  # Look for file-based configuration, additional to honoring configuration on the command line
  bees_config_dir="${bees_config_dir:-/etc/bees}"
  if [[ $bees_fsSpec =~ $uuid_re ]]; then
    bees_uuid=$bees_fsSpec
    # If our spec looks like a bare UUID, and no config file exists in the new
    # format, fall back to legacy config file search mechanism (grep; ewww).
    if ! readConfigFileIfExists "$bees_config_dir/UUID=$bees_fsSpec.conf"; then
      # Legacy approach to finding a config file: Grep for a *.conf file
      # containing the UUID within its text. Permitting spaces around the "="
      # appears to be a bug, but is retained for compatibility with the
      # original upstream script.
      allConfFiles=( "$bees_config_dir"/*.conf )
      if (( ${#allConfFiles[@]} )); then
        # in read or readarray with -d '', the NUL terminating the empty string is used as delimiter character.
        readarray -d '' -t matchingConfFiles < <(grep -E -l -Z "^[^#]*UUID[[:space:]]*=[[:space:]]*" "${allConfFiles[@]}")
      else
        matchingConfFiles=( )
      fi
      if (( ${#matchingConfFiles[@]} == 1 )); then
        # Exactly one configuration file exists in our target directory with a reference to the UUID given.
        bees_config_file=${matchingConfFiles[0]}
        readConfigFileIfExists "$bees_config_file"
        echo "NOTE: Please consider renaming $bees_config_file to $bees_config_dir/UUID=$bees_fsSpec" >&2
        echo "      ...and passing UUID=$bees_fsSpec on startup." >&2
      elif (( ${#matchingConfFiles[@]} > 1 )); then
        # The legacy wrapper would silently use the first file and ignore
        # others, but... no.
        echo "ERROR: Passed a bare UUID, but multiple configuration files match it:" >&2
        printf ' - %q\n' "${matchingConfFiles[@]}" >&2
        die "Unable to continue."
      fi
    fi
  else
    # For a non-UUID fsSpec that is not a path, look only for a config file
    # exactly matching its text.
    #
    # (Passing a mount point as a fsSpec is only supported with the new
    # wrapper; all key=value pairs can be passed on the command line in this
    # mode, so config file support is not needed).
    [[ $bees_fsSpec = */* ]] || readConfigFileIfExists "$bees_config_dir/$bees_fsSpec.conf"
  fi

  [[ $bees_uuid ]] || {
    # if bees_uuid is not in our .conf file, look it up with findmnt
    for findmnt_mode in --kernel --mtab --fstab; do
      read -r bees_uuid fstype < <(findmnt "$findmnt_mode" -n -o uuid,fstype "$bees_fsSpec") ||:
      [[ $bees_uuid && $fstype ]] && break
    done
    [[ $bees_uuid ]] || die "Unable to identify a device matching $bees_fsSpec with findmnt"
    [[ $fstype = btrfs ]] || die "Device type is $fstype, not btrfs"
  }

  [[ $bees_uuid = */* ]] || readConfigFileIfExists "$bees_config_dir/UUID=$bees_uuid.conf"

  # Honor any values read from config files above; otherwise, set defaults.
  bees_workDir="${bees_workDir:-.beeshome}"
  bees_runDir="${bees_runDir:-/run/bees}"
  bees_mntDir="${bees_mntDir:-$bees_runDir/mnt/$bees_uuid}"
  bees_home="${bees_home:-$bees_mntDir/$bees_workDir}"
  bees_status="${bees_status:-${bees_runDir}/$bees_uuid.status}"
  bees_verbosity="${bees_verbosity:-6}"
  bees_idxSizeMB="${bees_idxSizeMB:-1024}"
  bees_idxSize=${bees_idxSize:-"$(( bees_idxSizeMB * 1024 * 1024 ))"}
  bees_blockdev=${bees_blockdev:-"/dev/disk/by-uuid/$bees_uuid"}

  [[ -b $bees_blockdev ]] || die "Block device $bees_blockdev missing"
  (( bees_idxSize % (16 * 1024 * 1024) == 0 )) || die "DB size must be divisible by 16MB"
}

do_run() {
  local db old_db_size

  _setup "$1"; shift
  mkdir -p -- "$bees_mntDir" || exit

  # subvol id 5 is reserved for the root subvolume of a btrfs filesystem.
  mountpoint -q "$bees_mntDir" || mount -osubvolid=5 -- "$bees_blockdev" "$bees_mntDir" || exit
  if [[ -d $bees_home ]]; then
    btrfs subvolume show "$bees_home" >/dev/null 2>&1 || die "$bees_home exists but is not a subvolume"
  else
    btrfs subvolume create "$bees_home" || exit
    sync # workaround for Zygo/bees#93
  fi
  db=$bees_home/beeshash.dat
  touch -- "$db"

  old_db_size=$(stat -c %s -- "$db")
  new_db_size=$bees_idxSize

  if (( old_db_size != new_db_size )); then
    rm -f -- "$bees_home"/beescrawl."$bees_uuid".dat
    truncate -s "$new_db_size" -- "$db" || exit
  fi
  chmod 700 -- "$bees_home"

  # BEESSTATUS and BEESHOME are the only variables handled by the legacy
  # wrapper for which getenv() is called in C code.
  BEESSTATUS=$bees_status BEESHOME=$bees_home exec "${beesd_bin:-/lib/bees/bees}" \
    --verbose "$bees_verbosity" \
    "$@" "$bees_mntDir" || exit
}

do_cleanup() {
  _setup "$1"; shift
  mountpoint -q "$bees_mntDir" && umount -l -- "$bees_mntDir" || exit
}

(( $# >= 2 )) || usage
declare -f "do_$1" >/dev/null 2>&1 || usage
mode=$1; shift # must be a do_* function; currently "run" or "cleanup"

declare -a args=( "$1" ); shift  # pass first argument (config-name|fsSpec) through literally

# parse other arguments as key=value pairs, or pass them through literally if they do not match that form.
# similarly, any option after "--" will be passed through literally.
while (( $# )); do
  if [[ $1 = *=* ]]; then
    set_option "$1"
  elif [[ $1 = -- ]]; then
    shift
    args+=( "$@" )
    break
  else
    args+=( "$1" )
  fi
  shift
done

"do_$mode" "${args[@]}"
